package com.example.filter;

import com.example.entity.RestBean;
import com.example.utils.Const;
import com.example.utils.IpUtils;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * 限流过滤器，用于限制用户请求频率，防止刷接口
 *
 * @auth shanyue
 * @date 2025年06月29日21:08
 */
@Component
@Order(Const.ORDER_LIMIT)
public class FlowLimitFilter extends HttpFilter {

    private static final Logger log = LoggerFactory.getLogger(FlowLimitFilter.class);

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //本地缓存锁映射，避免高并发时对同一个key重复处理
    private static final Cache<String, Object> LOCK_MAP = CacheBuilder.newBuilder()
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build();

    //每个周期允许的最访问次数
    @Value("${flow.limit.threshold}")
    private int threshold;

    //限流判断周期
    @Value("${flow.limit.period.seconds}")
    private int periodSeconds;

    //封禁时间
    @Value("${flow.limit.block.seconds}")
    private int blockSeconds;

    /**
     * 获取指定key的锁对象，用于同步控制
     *
     * @param key key限流判断的唯一标识
     * @return 锁对象
     */
    private Object getLock(String key) {
        Object lock = LOCK_MAP.getIfPresent(key);
        if (lock == null) {
            lock = new Object();
            LOCK_MAP.put(key, lock);
        }
        return lock;
    }

    /**
     * 获取当前请求的限流key
     *
     * @param request HTTP请求对象
     * @return 用户限流的唯一标识
     */
    private String getLimiterKey(HttpServletRequest request) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) {
            return "user:" + auth.getName();
        } else {
            return "ip:" + getClientIp(request);
        }
    }


    /**
     * 获取客户端真实IP地址
     *
     * @param request 请求对象
     * @return IP地址
     */
    private String getClientIp(HttpServletRequest request) {
        return IpUtils.getClientIp(request);
    }

    /**
     * 核心过滤器逻辑，判断当前请求是否超过访问频率限制
     * 如果每页被限流，继续处理，如果被限流，返回错误响应
     *
     * @param request  当前HTTP请求对象
     * @param response 当前HTTP响应对象
     * @param chain    过滤器链，调用chain.doFilter()表示放行
     * @throws IOException      IO异常
     * @throws ServletException 其他异常
     */
    @Override
    protected void doFilter(HttpServletRequest request,
                            HttpServletResponse response,
                            FilterChain chain) throws IOException, ServletException {
        String key = getLimiterKey(request);
        try {
            if (tryCount(key)) {
                // 未被限流，继续执行后续请求链
                chain.doFilter(request, response);
            } else {
                // 被限流，返回限制响应
                writeBlockMessage(response, key);
            }
        } catch (Exception e) {
            log.error("限流过滤器异常，key: {}", key, e);
            // 出现异常时，为保证业务正常继续，放行请求
            chain.doFilter(request, response);
        }
    }

    /**
     * 判断请求是否可以继续执行，核心限流判断逻辑
     * 若已被封禁，直接拒接，否则通过计数器判断是否超过阈值
     *
     * @param key 当前请求的限流key
     * @return 是否允许通过
     */
    private boolean tryCount(String key) {
        synchronized (getLock(key)) {
            String blockKey = Const.FLOW_LIMIT_BLOCK + key;
            if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(blockKey))) {
                // 当前处于封禁状态，拒绝访问
                return false;
            }
            // 计数器自增并判断是否超过阈值
            return limitPeriodCheck(key);
        }
    }

    /**
     * 限流周期的计数器检查，判断是否超过访问阈值
     * 若超过阈值，添加封禁记录
     *
     * @param key 当前请求的限流key
     * @return 是否允许访问
     */
    private boolean limitPeriodCheck(String key) {
        String counterKey = Const.FLOW_LIMIT_COUNTER + key;

        //Redis自增
        Long count = stringRedisTemplate.opsForValue().increment(counterKey);
        if (count != null && count == 1) {
            // 第一次访问，设置计数器过期时间
            stringRedisTemplate.expire(counterKey, periodSeconds, TimeUnit.SECONDS);
        }
        if (count != null && count > threshold) {
            // 超过阈值，添加封禁标识
            stringRedisTemplate.opsForValue().set(Const.FLOW_LIMIT_BLOCK + key, "blocked", blockSeconds, TimeUnit.SECONDS);
            log.warn("Key [{}] 超过访问限制，封禁 {} 秒", key, blockSeconds);
            return false;
        }
        return true;
    }

    /**
     * 被限流时的响应写入方法
     * 设置HTTP状态码为429，并返回JSON格式提示信息
     *
     * @param response HTTP响应对象
     * @param key      限流用户标识
     * @throws IOException IO异常
     */
    private void writeBlockMessage(HttpServletResponse response, String key) throws IOException {
        response.setStatus(429);
        response.setHeader("Retry-After", String.valueOf(blockSeconds));
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(RestBean.forbidden("操作频繁, 请稍后再试").asJSONString());
        log.warn("Key [{}] 被限流，拒绝访问", key);
    }
}